/**************************************************************************************************
  Filename:       osal_snv.c
  Revised:        $Date: 2013-02-15 10:12:26 -0800 (Fri, 15 Feb 2013) $
  Revision:       $Revision: 33143 $

  Description:    This module contains the OSAL simple non-volatile memory functions.

 
**************************************************************************************************/

/*********************************************************************
 * INCLUDES
 */

#include "hal_adc.h"
#include "hal_flash.h"
#include "hal_types.h"
#include "comdef.h"
#include "OSAL.h"
#include "osal_snv.h"
#include "hal_assert.h"
#include "saddr.h"

#ifdef OSAL_SNV_UINT16_ID
# error "This OSAL SNV implementation does not support the extended ID space"
#endif

/*********************************************************************
 * CONSTANTS
 */

// NV page configuration
#define OSAL_NV_PAGE_SIZE       HAL_FLASH_PAGE_SIZE
#define OSAL_NV_PAGES_USED      HAL_NV_PAGE_CNT
#define OSAL_NV_PAGE_BEG        HAL_NV_PAGE_BEG
#define OSAL_NV_PAGE_END       (OSAL_NV_PAGE_BEG + OSAL_NV_PAGES_USED - 1)

// Default byte value when flash is erased
#define OSAL_NV_ERASED          0xFF

// NV page header size in bytes
#define OSAL_NV_PAGE_HDR_SIZE  4

// In case pages 0-1 are ever used, define a null page value.
#define OSAL_NV_PAGE_NULL       0

// In case item Id 0 is ever used, define a null item value.
#define OSAL_NV_ITEM_NULL       0

// Length in bytes of a flash word
#define OSAL_NV_WORD_SIZE       HAL_FLASH_WORD_SIZE

// NV page header offset within a page
#define OSAL_NV_PAGE_HDR_OFFSET 0


// Flag in a length field of an item header to indicate validity
// of the length field
#define OSAL_NV_INVALID_LEN_MARK 0x8000

// Flag in an ID field of an item header to indicate validity of
// the identifier field
#define OSAL_NV_INVALID_ID_MARK  0x8000


// Bit difference between active page state indicator value and
// transfer page state indicator value
#define OSAL_NV_ACTIVE_XFER_DIFF  0x00100000

// active page state indicator value
#define OSAL_NV_ACTIVE_PAGE_STATE OSAL_NV_ACTIVE_XFER_DIFF

// transfer page state indicator value
#define OSAL_NV_XFER_PAGE_STATE   (OSAL_NV_ACTIVE_PAGE_STATE ^ OSAL_NV_ACTIVE_XFER_DIFF)

#define OSAL_NV_MIN_COMPACT_THRESHOLD   70 // Minimum compaction threshold
#define OSAL_NV_MAX_COMPACT_THRESHOLD   95 // Maximum compaction threshold

/*********************************************************************
 * MACROS
 */

// Macro to check supply voltage
#if (defined HAL_MCU_CC2530 || defined HAL_MCU_CC2531)
# define  OSAL_NV_CHECK_BUS_VOLTAGE  (HalAdcCheckVdd(VDD_MIN_FLASH))
#elif defined HAL_MCU_CC2533
# define  OSAL_NV_CHECK_BUS_VOLTAGE  (HalBatMonRead( HAL_BATMON_MIN_FLASH ))
#else
// The radio chip does not support voltage monitoring
# define  OSAL_NV_CHECK_BUS_VOLTAGE TRUE
#endif

/*********************************************************************
 * TYPEDEFS
 */

// NV item header structure
typedef struct
{
  uint16 id;
  uint16 len;
} osalNvItemHdr_t;
// Note that osalSnvId_t and osalSnvLen_t cannot be bigger than uint16

/*********************************************************************
 * EXTERNAL FUNCTIONS
 */

extern bool HalAdcCheckVdd(uint8 limit);

/*********************************************************************
 * GLOBAL VARIABLES
 */

#ifndef OAD_KEEP_NV_PAGES
// When NV pages are to remain intact during OAD download,
// the image itself should not include NV pages.
#pragma location="BLENV_ADDRESS_SPACE"
__no_init uint8 _nvBuf[OSAL_NV_PAGES_USED * OSAL_NV_PAGE_SIZE];
#pragma required=_nvBuf
#endif // OAD_KEEP_NV_PAGES

#if defined MAKE_CRC_SHDW
#pragma location="CRC_SHDW"
const CODE uint16 _crcShdw = 0xFFFF;
#pragma required=_crcShdw
#endif

/*********************************************************************
 * LOCAL VARIABLES
 */

// active page
static uint8 activePg;

// active page offset
static uint16 pgOff;

// flag to indicate that an error has occurred while writing to or erasing the
// flash device. Once this flag indicates failure, it is unsafe to attempt
// another write or erase.
static uint8 failF;

/*********************************************************************
 * LOCAL FUNCTIONS
 */

static uint8  initNV( void );

static void   setActivePage( uint8 pg );
static void   setXferPage(void);
static void   erasePage( uint8 pg );
static void   cleanErasedPage( uint8 pg );
static void   findOffset( void );
static void   compactPage( uint8 pg );

static void   writeWord( uint8 pg, uint16 offset, uint8 *pBuf );
static void   writeWordM( uint8 pg, uint16 offset, uint8 *pBuf, osalSnvLen_t cnt );


// NOTE: Triggering erase upon power up may cause fast aging of the flash device
//       if there is power switch debounce issue, etc.
//       Improvement of this is to add a certain delay upon power up before
//       osal_nv_init() is called.

/*********************************************************************
 * @fn      initNV
 *
 * @brief   Initialize the NV flash pages.
 *
 * @param   none
 *
 * @return  TRUE if initialization succeeds. FALSE, otherwise.
 */
static uint8 initNV( void )
{
  uint32 pgHdr;
  uint8 xferPg = OSAL_NV_PAGE_NULL;
  uint8 pg;

  failF = FALSE;
  activePg = OSAL_NV_PAGE_NULL;

  // Pick active page and clean up erased page if necessary
  for ( pg = OSAL_NV_PAGE_BEG; pg <= OSAL_NV_PAGE_END; pg++ )
  {
    HalFlashRead(pg, OSAL_NV_PAGE_HDR_OFFSET, (uint8 *)(&pgHdr), OSAL_NV_PAGE_HDR_SIZE);

    if ( pgHdr == OSAL_NV_ACTIVE_PAGE_STATE)
    {
      if (activePg != OSAL_NV_PAGE_NULL)
      {
        // Both pages are active only when power failed during flash erase and
        // with very low probability.
        // As it is hard (code size intensive) to figure out which page is the real active page,
        // and theoretically impossible as well in lowest probability, erase both pages
        // in this case
        cleanErasedPage(activePg);
        cleanErasedPage(pg);
        activePg = OSAL_NV_PAGE_NULL;
      }
      else
      {
        activePg = pg;
      }
    }
    else if ( pgHdr == OSAL_NV_XFER_PAGE_STATE)
    {
      xferPg = pg;
    }
    else
    {
      // Erase this page if it is not erased.
      // This is to ensure that any page that were in the middle of
      // compacting gets erased.
      cleanErasedPage(pg);
    }
  }

  if (activePg == OSAL_NV_PAGE_NULL)
  {
    if (xferPg == OSAL_NV_PAGE_NULL)
    {
      // Both pages are erased. This must be initial state.
      // Pick one page as active page.
      setActivePage(OSAL_NV_PAGE_BEG);
      pgOff = OSAL_NV_PAGE_HDR_SIZE;

      // If setting active page from a completely erased page failed,
      // it is not recommended to operate any further.
      // Other cases, even if non-active page is corrupt, NV module can still read
      // the active page content and hence this function could return TRUE.
      return (!failF);
    }
    else
    {
      // Compacting a page hasn't completed in previous power cycle.
      // Complete the compacting.
      activePg = xferPg;
      findOffset();

      compactPage(xferPg);
    }
  }
  else
  {
    if (xferPg != OSAL_NV_PAGE_NULL)
    {
      // Compacting has completed except for the final step of erasing
      // the xferPage.
      erasePage(xferPg);
    }

    // find the active page offset to write a new variable location item
    findOffset();
  }

  return TRUE;
}

/*********************************************************************
 * @fn      setActivePage
 *
 * @brief   Set page header active state to be active.
 *
 * @param   pg - Valid NV page to activate.
 *
 * @return  none
 */
static void setActivePage( uint8 pg )
{
  uint32 pgHdr;

  pgHdr = OSAL_NV_ACTIVE_PAGE_STATE;

  writeWord( pg, OSAL_NV_PAGE_HDR_OFFSET, (uint8*) &pgHdr );
  if (!failF)
  {
    activePg = pg;
  }
}

/*********************************************************************
 * @fn      setXferPage
 *
 * @brief   Set active page header state to be transfer state.
 *
 * @param   none
 *
 * @return  none
 */
static void setXferPage(void)
{
  uint32 pgHdr;

  // erase difference bit between active state and xfer state
  pgHdr = OSAL_NV_XFER_PAGE_STATE;

  writeWord( activePg, OSAL_NV_PAGE_HDR_OFFSET, (uint8*)&pgHdr );
}

/*********************************************************************
 * @fn      erasePage
 *
 * @brief   Erases a page in Flash.
 *
 * @param   pg - Valid NV page to erase.
 *
 * @return  none
 */
static void erasePage( uint8 pg )
{
  if ( !OSAL_NV_CHECK_BUS_VOLTAGE || failF)
  {
    failF = TRUE;
    return;
  }

  HalFlashErase(pg);

  {
    // Verify the erase operation
    uint16 offset;
    uint8 tmp;

    for (offset = 0; offset < OSAL_NV_PAGE_SIZE; offset ++)
    {
      HalFlashRead(pg, offset, &tmp, 1);
      if (tmp != OSAL_NV_ERASED)
      {
        failF = TRUE;
        break;
      }
    }
  }
}

/*********************************************************************
 * @fn      cleanErasedPage
 *
 * @brief   Erases a page in Flash if the page is not completely erased.
 *
 * @param   pg - Valid NV page to erase.
 *
 * @return  none
 */
static void cleanErasedPage( uint8 pg )
{
  uint8 buf;
  uint16 offset;

  for (offset = 0; offset < OSAL_NV_PAGE_SIZE; offset ++)
  {
    HalFlashRead(pg, offset, &buf, 1);
    if (buf != OSAL_NV_ERASED)
    {
      erasePage(pg);
      break;
    }
  }
}

/*********************************************************************
 * @fn      findOffset
 *
 * @brief   find an offset of an empty space in active page
 *          where to write a new item to.
 *
 * @param   None
 *
 * @return  none
 */
static void findOffset(void)
{
  uint16 offset;
  for (offset = OSAL_NV_PAGE_SIZE - OSAL_NV_WORD_SIZE;
       offset >= OSAL_NV_PAGE_HDR_SIZE;
       offset -= OSAL_NV_WORD_SIZE)
  {
    uint32 tmp;

    HalFlashRead(activePg, offset, (uint8 *)&tmp, OSAL_NV_WORD_SIZE);
    if (tmp != 0xFFFFFFFF)
    {
      break;
    }
  }
  pgOff = offset + OSAL_NV_WORD_SIZE;
}

/*********************************************************************
 * @fn      findItem
 *
 * @brief   find a valid item from a designated page and offset
 *
 * @param   pg       - NV page
 * @param   offset   - offset in the NV page from where to start
 *                     search up.
 *                     Usually this paramter is set to the empty space
 *                     offset.
 * @param   id       - NV item ID to search for
 *
 * @return  offset of the item, 0 when not found
 */
static uint16 findItem(uint8 pg, uint16 offset, osalSnvId_t id)
{
  offset -= OSAL_NV_WORD_SIZE;

  while (offset >= OSAL_NV_PAGE_HDR_SIZE)
  {
    osalNvItemHdr_t hdr;

    HalFlashRead(pg, offset, (uint8 *) &hdr, OSAL_NV_WORD_SIZE);

    if (hdr.id == id)
    {
      // item found
      // length field could be corrupt. Mask invalid length mark.
      uint8 len = hdr.len & ~OSAL_NV_INVALID_LEN_MARK;
      return offset - len;
    }
    else if (hdr.len & OSAL_NV_INVALID_LEN_MARK)
    {
      offset -= OSAL_NV_WORD_SIZE;
    }
    else
    {
      // valid length field
      if (hdr.len + OSAL_NV_WORD_SIZE <= offset)
      {
        // valid length
        offset -= hdr.len + OSAL_NV_WORD_SIZE;
      }
      else
      {
        // active page is corrupt
        // This could happen if NV initialization failed upon failure to erase
        // page and active page is set to uncleanly erased page.
        HAL_ASSERT_FORCED();
        return 0;
      }
    }
  }
  return 0;
}

/*********************************************************************
 * @fn      writeItem
 *
 * @brief   Write a data item to NV. Function can write an entire item to NV
 *
 * @param   pg     - Page number
 * @param   offset - offset within the NV page where to write the new item
 * @param   id     - NV item ID
 * @param   alignedLen - Length of data to write, alinged in flash word
 *                       boundary
 * @param  *pBuf   - Data to write.
 *
 * @return  none
 */
static void writeItem( uint8 pg, uint16 offset, osalSnvId_t id, uint16 alignedLen, uint8 *pBuf )
{
  osalNvItemHdr_t hdr;

  hdr.id = 0xFFFF;
  hdr.len = alignedLen | OSAL_NV_INVALID_LEN_MARK;

  // Write the len portion of the header first
  writeWord(pg, offset + alignedLen, (uint8 *) &hdr);

  // remove invalid len mark
  hdr.len &= ~OSAL_NV_INVALID_LEN_MARK;
  writeWord(pg, offset + alignedLen, (uint8 *) &hdr);

  // Copy over the data
  writeWordM(pg, offset, pBuf, alignedLen / OSAL_NV_WORD_SIZE);

  // value is valid. Write header except for the most significant bit.
  hdr.id = id | OSAL_NV_INVALID_ID_MARK;
  writeWord(pg, offset + alignedLen, (uint8 *) &hdr);

  // write the most significant bit
  hdr.id &= ~OSAL_NV_INVALID_ID_MARK;
  writeWord(pg, offset + alignedLen, (uint8 *) &hdr);
}

/*********************************************************************
 * @fn      xferItem
 *
 * @brief   Copy an NV item from the active page to a designated page.
 *
 * @param   pg         - NV page where to copy the item to.
 * @param   offset     - NV page offset where to copy the item to.
 * @param   alignedLen - Length of data to write, aligned in flash word
 *                       boundary.
 * @param   srcOff     - NV page offset of the original data in active page
 *
 * @return  none.
 */
static void xferItem( uint8 pg, uint16 offset, uint16 alignedLen, uint16 srcOff )
{
  uint8 tmp[OSAL_NV_WORD_SIZE];
  uint16 i = 0;

  // Copy over the data
  while (i <= alignedLen)
  {
    HalFlashRead(activePg, srcOff + i, tmp, OSAL_NV_WORD_SIZE);
    writeWord(pg, offset + i, tmp);

    i += OSAL_NV_WORD_SIZE;
  }
}

/*********************************************************************
 * @fn      compactPage
 *
 * @brief   Compacts the page specified.
 *
 * @param   srcPg - Valid NV page to compact from.
 *                  The page must have changed its state (header) to xfer state
 *                  prior to this function call. This function will not
 *                  modify the state of its header to xfer state before starting
 *                  to compact.
 *
 * @return  none.
 */
static void compactPage( uint8 srcPg )
{
  uint16 srcOff, dstOff;
  uint8 dstPg;
  osalSnvId_t lastId = (osalSnvId_t) 0xFFFF;

  dstPg = (srcPg == OSAL_NV_PAGE_BEG)? OSAL_NV_PAGE_END : OSAL_NV_PAGE_BEG;

  dstOff = OSAL_NV_PAGE_HDR_SIZE;

  // Read from the latest value
  srcOff = pgOff - sizeof(osalNvItemHdr_t);

  while (srcOff >= OSAL_NV_PAGE_HDR_SIZE)
  {
    osalNvItemHdr_t hdr;

    if (failF)
    {
      // Failure during transfer item will make next findItem error prone.
      return;
    }

    HalFlashRead(srcPg, srcOff, (uint8 *) &hdr, OSAL_NV_WORD_SIZE);

    if (hdr.id == 0xFFFF)
    {
      // Invalid entry. Skip this one.
      if (hdr.len & OSAL_NV_INVALID_LEN_MARK)
      {
        srcOff -= OSAL_NV_WORD_SIZE;
      }
      else
      {
        if (hdr.len + OSAL_NV_WORD_SIZE <= srcOff)
        {
          srcOff -= hdr.len + OSAL_NV_WORD_SIZE;
        }
        else
        {
          // invalid length. Source page must be a corrupt page.
          // This is possible only if the NV initialization failed upon erasing
          // what is selected as active page.
          // This is supposed to be a very rare case, as power should be
          // shutdown exactly during erase and then the page header is
          // still retained as either the Xfer or the Active state.

          // For production code, it might be useful to attempt to erase the page
          // so that at next power cycle at least the device is runnable
          // (with all entries removed).
          // However, it might be still better not to attempt erasing the page
          // just to see if this very rare case actually happened.
          //erasePage(srcPg);

          HAL_ASSERT_FORCED();
          return;
        }
      }

      continue;
    }

    // Consider only valid item
    if (!(hdr.id & OSAL_NV_INVALID_ID_MARK) && hdr.id != lastId)
    {
      // lastId is used to speed up compacting in case the same item ID
      // items were neighboring each other contiguously.
      lastId = (osalSnvId_t) hdr.id;

      // Check if the latest value of the item was already written
      if (findItem(dstPg, dstOff, lastId) == 0)
      {
        // This item was not copied over yet.
        // This must be the latest value.
        // Write the latest value to the destination page

        xferItem(dstPg, dstOff, hdr.len, srcOff - hdr.len);

        dstOff += hdr.len + OSAL_NV_WORD_SIZE;
      }
    }
    srcOff -= hdr.len + OSAL_NV_WORD_SIZE;
  }

  // All items copied.
  // Activate the new page
  setActivePage(dstPg);

  if (!failF)
  {
    pgOff = dstOff; // update active page offset
  }

  // Erase the currently active page
  erasePage(srcPg);
}

/*********************************************************************
 * @fn      verifyWordM
 *
 * @brief   verify the written word.
 *
 * @param   pg - A valid NV Flash page.
 * @param   offset - A valid offset into the page.
 * @param   pBuf - Pointer to source buffer.
 * @param   cnt - Number of 4-byte blocks to verify.
 *
 * @return  none
 */
static void verifyWordM( uint8 pg, uint16 offset, uint8 *pBuf, osalSnvLen_t cnt )
{
  uint8 tmp[OSAL_NV_WORD_SIZE];

  while (cnt--)
  {
    // Reading byte per byte will reduce code size but will slow down
    // and not sure it will meet the timing requirements.
    HalFlashRead(pg, offset, tmp, OSAL_NV_WORD_SIZE);
    if (FALSE == osal_memcmp(tmp, pBuf, OSAL_NV_WORD_SIZE))
    {
      failF = TRUE;
      return;
    }
    offset += OSAL_NV_WORD_SIZE;
    pBuf += OSAL_NV_WORD_SIZE;
  }
}

/*********************************************************************
 * @fn      writeWord
 *
 * @brief   Writes a Flash-WORD to NV.
 *
 * @param   pg - A valid NV Flash page.
 * @param   offset - A valid offset into the page.
 * @param   pBuf - Pointer to source buffer.
 *
 * @return  none
 */
static void writeWord( uint8 pg, uint16 offset, uint8 *pBuf )
{
  uint16 addr = (offset >> 2) + ((uint16)pg << 9);

  if ( !failF )
  {
    HalFlashWrite(addr, pBuf, 1);
    verifyWordM(pg, offset, pBuf, 1);
  }
}

/*********************************************************************
 * @fn      writeWordM
 *
 * @brief   Writes multiple Flash-WORDs to NV.
 *
 * @param   pg - A valid NV Flash page.
 * @param   offset - A valid offset into the page.
 * @param   buf - Pointer to source buffer.
 * @param   cnt - Number of 4-byte blocks to write.
 *
 * @return  none
 */
static void writeWordM( uint8 pg, uint16 offset, uint8 *buf, osalSnvLen_t cnt )
{
  uint16 addr = (offset >> 2) + ((uint16)pg << 9);

  if ( !failF )
  {
    HalFlashWrite(addr, buf, cnt);
    verifyWordM(pg, offset, buf, cnt);
  }
}

/*********************************************************************
 * @fn      osal_snv_init
 *
 * @brief   Initialize NV service.
 *
 * @return  SUCCESS if initialization succeeds. FAILURE, otherwise.
 */
uint8 osal_snv_init( void )
{
  if (!initNV())
  {
    // NV initialization failed
    HAL_ASSERT_FORCED();

    return FAILURE;
  }

  return SUCCESS;
}

/*********************************************************************
 * @fn      osal_snv_write
 *
 * @brief   Write a data item to NV.
 *
 * @param   id  - Valid NV item Id.
 * @param   len - Length of data to write.
 * @param   *pBuf - Data to write.
 *
 * @return  SUCCESS if successful, NV_OPER_FAILED if failed.
 */
uint8 osal_snv_write( osalSnvId_t id, osalSnvLen_t len, void *pBuf )
{
  uint16 alignedLen;

  {
    uint16 offset = findItem(activePg, pgOff, id);

    if (offset > 0)
    {
      uint8 tmp;
      osalSnvLen_t i;

      for (i = 0; i < len; i++)
      {
        HalFlashRead(activePg, offset, &tmp, 1);
        if (tmp != ((uint8 *)pBuf)[i])
        {
          break;
        }
        offset++;
      }

      if (i == len)
      {
        // Changed value is the same value as before.
        // Return here instead of re-writing the same value to NV.
        return SUCCESS;
      }
    }
  }

  alignedLen = ((len + OSAL_NV_WORD_SIZE - 1) / OSAL_NV_WORD_SIZE) * OSAL_NV_WORD_SIZE;

  if ( pgOff + alignedLen + OSAL_NV_WORD_SIZE > OSAL_NV_PAGE_SIZE )
  {
    setXferPage();
    compactPage(activePg);
  }

  // pBuf shall be referenced beyond its valid length to save code size.
  writeItem(activePg, pgOff, id, alignedLen, pBuf);
  if (failF)
  {
    return NV_OPER_FAILED;
  }

  pgOff += alignedLen + OSAL_NV_WORD_SIZE;

  return SUCCESS;
}

/*********************************************************************
 * @fn      osal_snv_read
 *
 * @brief   Read data from NV.
 *
 * @param   id  - Valid NV item Id.
 * @param   len - Length of data to read.
 * @param   *pBuf - Data is read into this buffer.
 *
 * @return  SUCCESS if successful.
 *          Otherwise, NV_OPER_FAILED for failure.
 */
uint8 osal_snv_read( osalSnvId_t id, osalSnvLen_t len, void *pBuf )
{
  uint16 offset = findItem(activePg, pgOff, id);

  if (offset != 0)
  {
    HalFlashRead(activePg, offset, pBuf, len);
    return SUCCESS;
  }
  return NV_OPER_FAILED;
}

/*********************************************************************
 * @fn      osal_snv_compact
 *
 * @brief   Compacts NV if its usage has reached a specific threshold.
 *
 * @param   threshold - compaction threshold
 *
 * @return  SUCCESS if successful,
 *          NV_OPER_FAILED if failed, or
 *          INVALIDPARAMETER if threshold invalid.
 */
uint8 osal_snv_compact( uint8 threshold )
{
  if ( ( threshold < OSAL_NV_MIN_COMPACT_THRESHOLD ) ||
       ( threshold > OSAL_NV_MAX_COMPACT_THRESHOLD ) )
  {
    return INVALIDPARAMETER;
  }

  // See if NV active page usage has reached compaction threshold
  if ( ( (uint32)pgOff * 100 ) >= ( OSAL_NV_PAGE_SIZE * (uint32)threshold ) )
  {
    setXferPage();
    compactPage(activePg);

    return SUCCESS;
  }

  return NV_OPER_FAILED;
}

/*********************************************************************
*********************************************************************/
